6-3 进阶镜像构建多过程优化(corepack使用pnpm)
镜像体积分析
使用 docker images 查看构建结果,初始镜像约 800MB。第一次构建的镜像比第二次略小,原因在于自定义的 node:18-alpine 基础镜像中包含了 pnpm 的存储文件,而最终运行时并不需要 pnpm 本身及其安装过程中产生的缓存文件。
pnpm 官方 Dockerfile 最佳实践
pnpm 官方文档的 Recipes -> Working with Docker 提供了两个示例:
- 单服务项目的 Dockerfile
- **Monorepo(monorepo workspace)**的 Dockerfile
官方示例使用 node:20-slim 作为基础镜像,而非 Alpine。
Alpine vs Slim 镜像对比
| 特性 | Alpine | Slim |
|---|---|---|
| 基础系统 | Alpine Linux(专为 Docker 设计) | Debian(开源发行版) |
| 体积 | 更小 | 略大于 Alpine |
| 包管理器 | apk | apt-get |
| Corepack 支持 | 需手动配置 | 内置支持 |
| glibc | musl libc | glibc |
官方选择 slim 而非 alpine 的原因:slim 镜像内置 corepack(Node.js 16+ 自带),可以直接激活 pnpm,无需手动下载安装。
四阶段构建架构
参考官方示例,将 Dockerfile 重构为四个阶段:
base -> prod-deps -> build -> final
text
阶段一:Base(基础环境)
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-slim AS base
# 激活 corepack,支持 pnpm
RUN corepack enable
# 配置 pnpm 全局存储路径(使用缓存目录)
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
WORKDIR /app
dockerfile
corepack enable 会自动识别并激活 pnpm,跳过手动安装步骤。PNPM_HOME 指向缓存目录,使依赖安装的缓存可以在不同阶段间共享。
阶段二:Prod Deps(生产依赖)
FROM base AS prod-deps
# 先复制依赖声明文件
COPY --chown=node:node package*.json pnpm-lock.yaml ./
# 设置淘宝镜像源
RUN pnpm config set registry https://registry.npmmirror.com
# 安装生产依赖
RUN pnpm install --prod --frozen-lockfile
dockerfile
先安装生产依赖(--prod),因为 dependencies 和 devDependencies 可能存在共同依赖包。共享缓存可以让部分依赖包加速安装。
阶段三:Build(构建)
FROM base AS build
# 先安装全部依赖(包含 devDependencies)
COPY --chown=node:node package*.json pnpm-lock.yaml ./
RUN pnpm config set registry https://registry.npmmirror.com
RUN pnpm install --frozen-lockfile
# 复制源码
COPY --chown=node:node . .
# 生成 Prisma 客户端
RUN pnpm run build:clients
# 编译项目
RUN pnpm run build:prod
dockerfile
阶段四:Final(最终运行镜像)
FROM base AS final
# 设置工作目录权限
RUN mkdir -p /app && chown -R node:node /app
USER node
WORKDIR /app
# 从 prod-deps 阶段拷贝 node_modules(不含缓存文件)
COPY --from=prod-deps --chown=node:node /app/node_modules ./node_modules
# 从 build 阶段拷贝 Prisma 客户端
COPY --from=build --chown=node:node /app/prisma ./prisma
# 从 build 阶段拷贝编译产物
COPY --from=build --chown=node:node /app/dist ./dist
CMD ["node", "dist/main.js"]
dockerfile
关键优化点:为什么不直接使用 prod-deps 阶段作为最终镜像
pnpm 在安装依赖时会产生大量缓存文件(存储在 .pnpm 目录和 store 中)。如果直接 FROM prod-deps 作为最终镜像,这些缓存文件会被包含在内。
通过 COPY --from=prod-deps 只拷贝 node_modules 目录,缓存文件被排除在外。实测表明:
- 使用
COPY方式:约 526MB - 直接使用
FROM prod-deps:约 826MB - 差异约 300MB,即 pnpm 安装过程中产生的缓存文件大小
构建与验证
# 构建优化后的镜像
docker build -t nest-starter:1.3 .
# 查看镜像大小
docker images | grep nest
# nest-starter 1.3 约 526MB(优化前 800MB+)
bash
Alpine + Corepack 的替代方案
除了使用 slim 镜像,也可以使用之前封装的自定义 Alpine 基础镜像作为 build 阶段的基础,最终运行阶段使用 node:alpine 镜像,这样最终镜像会更小。这是留给读者的练习。
小结
- 官方推荐使用
corepack enable激活 pnpm,无需手动下载安装 - 四阶段构建(base -> prod-deps -> build -> final)清晰分离关注点
- 从
prod-deps阶段拷贝node_modules,排除 pnpm 缓存文件,减小约 300MB slim镜像内置 corepack,alpine镜像体积更小但需手动处理- 利用
--chown=node:node确保文件权限正确
↑